/*
 * Copyright 2016 Alibaba Group Holding Limited
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 * Boston, MA  02111-1307  USA
 */
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <sys/inotify.h>
#include <poll.h>
#include <sys/eventfd.h>
#include <list>
#include <string>
#include "properties.h"
#include "utils.h"

#undef LOG_TAG
#define LOG_TAG "libproperties.watch"

using namespace std;
#define anypointer (void *)-1

struct Callback {
    void invoke(const char *key, void *user_data) {
        //LOG_I("< invoke callback (%p,%p) with '%s' %p", callback_with_key_arg, callback, key, user_data);
        if (callback_with_key_arg != nullptr) {
			callback_with_key_arg(key, user_data);
		} else if (callback != nullptr) {
			callback(user_data);
		}else{ //LOG_E("null callback can't be invoked!");
        //LOG_I("> invoke callback (%p,%p) with '%s'", callback_with_key_arg, callback, key);
			printf("123");
		}
    }
    void *pointer() const {
        if (callback_with_key_arg != nullptr) return (void *)callback_with_key_arg;
        return (void *)callback;
    }
    bool is_null() const {
        return pointer() == nullptr;
    }
    bool is_matched(const Callback &r) const {
        return r.is_null() || *this == r;
    }
    bool operator==(const Callback &r) const {
        return this->pointer() == r.pointer();
    }

    Callback(property_callback cb): callback_with_key_arg(cb), callback(nullptr) {}
    Callback(property_callback_fn cb): callback_with_key_arg(nullptr), callback(cb) {}
    Callback(): callback_with_key_arg(nullptr), callback(nullptr) {}

private:
    property_callback callback_with_key_arg;
    property_callback_fn callback;
};


struct WatchRecord {
    int wd;
    string key;
    Callback callback;
    void *user_data;

    WatchRecord(int wd, const char *key, Callback callback, void *data):
            wd(wd), key(key), callback(callback), user_data(data) {}

    bool is_matched(const char *key, const Callback &callback, void *data) const {
        bool data_matched = data == anypointer || this->user_data == data;
        return data_matched && equal(this->key.c_str(), key) && this->callback.is_matched(callback);
    }
};

static pthread_t g_watch_thread_id = 0;
static int g_inotify_fd = -1;
static int g_thread_exit_fd = -1;
static list<WatchRecord> g_watch_list;
static pthread_mutex_t g_watch_list_mutex = PTHREAD_MUTEX_INITIALIZER;
static string g_watch_dir = "/dev/properties.watch/";

static bool contains_watch_record(const char *key, Callback callback, void *user_data) {
    for (auto &&entry: g_watch_list) {
        if (entry.is_matched(key, callback, user_data)) return true;
    }
    return false;
}
static bool add_watch_record(int wd, const char *key, Callback callback, void *data) {
    WatchRecord record(wd, key, callback, data);
    g_watch_list.push_back(record);
    return true;
}
static int remove_watch_record(const char *key, Callback cb, void *user_data) {
    int wd = -1;
    g_watch_list.remove_if([key, cb, user_data, &wd](WatchRecord &entry) {
        bool result = entry.is_matched(key, cb, user_data);
        if (result) wd = entry.wd;
        return result;
    });
    return wd;
}
// __attribute__((unused)) static void run_callbacks_locked(int wd) {
//     list<WatchRecord> watchList = g_watch_list;
//     for (auto itr = watchList.begin(); itr != watchList.end(); ++itr) {
//         if (itr->wd != wd) continue;

//         const char *key = itr->key.c_str();
//         auto callback = itr->callback;
//         auto user_data = itr->user_data;
//         if (contains_watch_record(key, callback, user_data)) {
//             //LOG_I("running callback of `%s` %p", key, callback.pointer());
//             callback.invoke(key, user_data);
//         } else {
//             //LOG_I("watch item has been removed, skip, %s %p", key, callback.pointer());
//         }
//     }
// }
static void run_callbacks(list<WatchRecord> &watch_list, int wd) {
    //LOG_D("run_callbacks %d", watch_list.size());
    for (auto &&entry: watch_list) {
        if (entry.wd != wd) continue;

        const char *key = entry.key.c_str();
        auto callback = entry.callback;
        callback.invoke(key, entry.user_data);
    }
}

// __attribute__((unused)) static void dump_watch_list() {
//     int i = 0, count = g_watch_list.size();
//     for (auto itr = g_watch_list.begin(); itr != g_watch_list.end(); ++itr) {
//         //LOG_I("watch item %d/%d %d `%s` %p", ++i, count, itr->wd, itr->key.c_str(), itr->callback.pointer());
//     }
// }

static int get_next_changed_wd() {
    struct pollfd pollfds[] = {
        { g_inotify_fd, POLLIN, },
        { g_thread_exit_fd, POLLIN, }
    };
    int nfds = sizeof(pollfds) / sizeof(pollfds[0]);
    int result = poll(pollfds, nfds, -1);
    if (result == -1) {
        //ERRNO("poll");
        if (errno == EINTR) {
            return -2;
        }
        return -1;
    }
    if (pollfds[1].revents & POLLIN) {
        //LOG_D("request to exit the watch thread.");
        return -1;
    }

    struct inotify_event event;
    int size = sizeof(event);
    int len = read(g_inotify_fd, &event, size);
    if (len == -1 && errno == EBADF) {
        //ERRNO("read");
        return -1;
    }

    if (len != size) {
        //ERROR("read incomplete inotify event");
        return -2;
    }
    if (!(event.mask & IN_ATTRIB)) return -2;
    return event.wd;
}
static void *main_watch_thread(void *args) {
    //LOG_I("watch thread started");
    while (true) {
        int wd = get_next_changed_wd();
        if (wd == -1) break;
        if (wd == -2) continue;

        pthread_mutex_lock(&g_watch_list_mutex);
        list<WatchRecord> watch_list_copy = g_watch_list;
        pthread_mutex_unlock(&g_watch_list_mutex);

        run_callbacks(watch_list_copy, wd);
    }
    //LOG_I("watch thread stopped");
    return NULL;
}

void property_unwatch_all();
void at_fork_child(void) {
    // //LOG_D("reset all property watch data structures in child process.");
    g_watch_thread_id = 0;
    if (g_inotify_fd != -1) {
        close(g_inotify_fd);
        g_inotify_fd = -1;
    }
    if (g_thread_exit_fd != -1) {
        close(g_thread_exit_fd);
        g_thread_exit_fd = -1;
    }
    g_watch_list.clear();
    pthread_mutex_unlock(&g_watch_list_mutex);
}
void at_fork_parent(void) {
    //LOG_D("parent process release lock.");
    pthread_mutex_unlock(&g_watch_list_mutex);
}
void at_fork_prepare(void) {
    //LOG_D("ensure there's no ongoing property ops.");
    pthread_mutex_lock(&g_watch_list_mutex);
}

static bool init_watch() {
    if (g_watch_thread_id != 0) return true;

    g_inotify_fd = inotify_init();
    if (g_inotify_fd == -1) {
        //ERRNO("inotify_init");
        return false;
    }

    g_thread_exit_fd = eventfd(0, EFD_CLOEXEC);
    if (g_thread_exit_fd == -1) //ERRNO("eventfd");

    int result = pthread_create(&g_watch_thread_id, NULL, main_watch_thread, NULL);
    //if (result != 0) {    //TODO
    //    //ERRNO("pthread_create");
    //    return false;
    //}

    static bool exit_handler_installed = false;
    if (!exit_handler_installed) {
        atexit(property_unwatch_all);
        pthread_atfork(at_fork_prepare, at_fork_parent, at_fork_child);
        exit_handler_installed = true;
    }

    pthread_setname_np(g_watch_thread_id, "PropertyWatch");
    //LOG_I("watch initialized");
    return true;
}
__attribute__((unused)) static void deinit_watch() {
    if (g_watch_thread_id == 0) return;

    uint64_t inc = 1;
    int result = write(g_thread_exit_fd, &inc, sizeof(inc));
    if (result != sizeof(inc)) {
        //ERRNO("write result %d", result);
        return;
    }
    pthread_join(g_watch_thread_id, NULL);
    g_watch_thread_id = 0;

    if (g_thread_exit_fd != -1) {
        close(g_thread_exit_fd);
        g_thread_exit_fd = -1;
    }

    close(g_inotify_fd);
    g_inotify_fd = -1;
    //LOG_I("watch destroyed");
}

static int add_watch_inotify(const char *key) {
    string path = g_watch_dir + key;
    const char *file = path.c_str();
    if (access(file, R_OK) == -1) {
        //LOG_D("create property file for %s", key);
        property_set(key, "initbywatch");
    }
    //LOG_D("inotify_add_watch %s", key);
    int wd = inotify_add_watch(g_inotify_fd, file, IN_ATTRIB);
    if (wd == -1) {
        //ERRNO("inotify_add_watch %s", file);
        return -1;
    }
    return wd;
}
static int property_watch_locked(const char *key, Callback cb, void *user_data) {
    if (contains_watch_record(key, cb, user_data)) {
        //LOG_W("the key and callback is in the watch list already.");
        return -1;
    }

    if (g_watch_list.size() == 0 && !init_watch()) return -1;

    int wd = add_watch_inotify(key);
    if (wd == -1) return -1;

    add_watch_record(wd, key, cb, user_data);
    return 0;
}
static int property_watch_internal(const char *key, Callback cb, void *user_data) {
    //LOG_I("property_watch_internal %s %p %p", key, cb.pointer(), user_data);
    if (key == NULL || cb.is_null()) {
        //LOG_E("key and callback can't be NULL");
        return -1;
    }

    pthread_mutex_lock(&g_watch_list_mutex);
    int result = property_watch_locked(key, cb, user_data);
    pthread_mutex_unlock(&g_watch_list_mutex);
    return result;
}
int property_watch(const char *key, property_callback cb, void *user_data) {
    //LOG_D("< property_watch %s %p", key, cb);
    int result = property_watch_internal(key, Callback(cb), user_data);
    //LOG_D("> property_watch %s %p, %d", key, cb, result);
    return result;
}
int property_add_watch(const char *key, property_callback_fn cb, void *user_data) {
    //LOG_D("< property_add_watch %s %p", key, cb);
    int result = property_watch_internal(key, Callback(cb), user_data);
    //LOG_D("> property_add_watch %s %p, %d", key, cb, result);
    return result;
}

static int property_unwatch_locked(const char *key, Callback cb, void *user_data) {
    int wd = remove_watch_record(key, cb, user_data);
    if (wd == -1) {
        //LOG_W("no watch record found");
        return -1;
    }

    if (contains_watch_record(key, Callback(), anypointer)) return 0;

    //LOG_D("inotify_rm_watch %s", key);
    int result = inotify_rm_watch(g_inotify_fd, wd);
    if (result != 0) //ERRNO("inotify_rm_watch");

    // do not deinit_watch, to avoid potential init/deinit cycle,
    // the cost is a thread and an inotify instance.
    // if (g_watch_list.size() == 0) {
    //     deinit_watch();
    // }
    return 0;
}
static int property_unwatch_internal(const char *key, Callback cb, void *user_data) {
    //LOG_I("property_unwatch_internal %s %p %p", key, cb.pointer(), user_data);
    if (key == NULL) {
        //LOG_E("key can't be NULL");
        return -1;
    }

    pthread_mutex_lock(&g_watch_list_mutex);
    int result = property_unwatch_locked(key, cb, user_data);
    pthread_mutex_unlock(&g_watch_list_mutex);
    return result;
}
int property_unwatch(const char *key, property_callback cb, void *user_data) {
    //LOG_D("< property_unwatch %s %p", key, cb);
    int result = property_unwatch_internal(key, Callback(cb), user_data);
    //LOG_D("> property_unwatch %s %p, %d", key, cb, result);
    return result;
}
int property_rm_watch_by_callback(const char *key, property_callback_fn cb) {
    //LOG_D("< property_rm_watch_by_callback %s %p", key, cb);
    int result = property_unwatch_internal(key, Callback(cb), anypointer);
    //LOG_D("> property_rm_watch_by_callback %s %p, %d", key, cb, result);
    return result;
}
int property_rm_watch(const char *key) {
    //LOG_D("< property_rm_watch %s", key);
    int result = property_unwatch_internal(key, Callback(), anypointer);
    //LOG_D("> property_rm_watch %s, %d", key, result);
    return result;
}

void property_unwatch_all() {
    //LOG_D("< property_unwatch_all");
    pthread_mutex_lock(&g_watch_list_mutex);
    g_watch_list.clear();
    pthread_mutex_unlock(&g_watch_list_mutex);
    deinit_watch();
    //LOG_D("> property_unwatch_all");
}

